// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/bybit_futures/api_order/native_private_client.h"

#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/bybit_futures/api_util/rate_tracker.h"
#include "coin2/exchange/bybit_futures/symbology/product.h"

void BybitFuturesNativePrivateClient::SetMakerOrder(bool is_maker_order) {
  local_rate_tracker_->SetMakerOrder(is_maker_order);
}

auto BybitFuturesNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  using coin2::exchange::bybit_futures::symbology::BybitFuturesProduct;
  auto product = BybitFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_symbol(order_spec.product);
  params.set_price(formatter.FormatPrice(order_spec.price));
  params.set_qty(formatter.FormatQty(order_spec.qty));
  params.set_order_link_id(std::to_string(proc_order_id));

  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_order_type("Market");
  } else {
    params.set_order_type("Limit");
  }

  if (order_spec.post_only) {
    params.set_time_in_force("PostOnly");
  } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_time_in_force("FillOrKill");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("ImmediateOrCancel");
  } else {
    params.set_time_in_force("GoodTillCancel");
  }

  if (is_linear_) {
    if (order_spec.order_side == OrderSide::BUY_OPEN_ORDER) {
      params.set_side("Buy");
      params.set_reduce_only("false");
    } else if (order_spec.order_side == OrderSide::SELL_OPEN_ORDER) {
      params.set_side("Sell");
      params.set_reduce_only("false");
    } else if (order_spec.order_side == OrderSide::SELL_CLOSE_ORDER) {
      params.set_side("Sell");
      params.set_reduce_only("true");
    } else if (order_spec.order_side == OrderSide::BUY_CLOSE_ORDER) {
      params.set_side("Buy");
      params.set_reduce_only("true");
    } else {
      NOTREACHED() << "[Bybit linear] Unknown order side: "
                   << static_cast<int>(order_spec.order_side);
    }
  } else {
    if (order_spec.order_side == OrderSide::BUY_ORDER) {
      params.set_side("Buy");
    } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
      params.set_side("Sell");
    } else {
      NOTREACHED() << "[Bybit futures] Unknown order side: "
                   << static_cast<int>(order_spec.order_side);
    }
  }
  return params;
}

auto BybitFuturesNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  params.set_symbol(order_context.order_spec().product);
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_order_link_id(std::to_string(order_context.proc_order_id()));
  }
  return params;
}

BybitFuturesNativePrivateClient::BybitFuturesNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override,
    bool is_linear) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("api.bybit.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[BybitFuturesNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<BybitRestAuth, NativeClientConfig>(config);

  is_linear_ = is_linear;

  rate_tracker_ = std::make_unique<BybitRateTracker>(*this, mea_, config);
  local_rate_tracker_ = static_cast<BybitRateTracker*>(rate_tracker_.get());
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::QueryBalance() {
  auto ts = GetCurrentTimestamp();
  // from test result, if query balance with no specify symbol, default charged BTC rate
  auto native_symbol = is_linear_ ? "BTCUSDT" : "BTCUSD";
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryAccount())) {
    LOG(ERROR) << "Hit rate limit. QueryBalance failed";
    return {};
  }
  UrlParamsBuilder builder;
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/v2/private/wallet/balance", &builder);
  Push(1, ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryAccount());

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::QueryPosition(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  const auto& native_symbol = params.symbol();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryAccount())) {
    LOG(ERROR) << "Hit rate limit. QueryPosition failed";
    return {};
  }
  std::string position_path = "/private/linear/position/list";
  if (!is_linear_) {
    if (IsDeliveryNativeSymbol(native_symbol)) {
      position_path = "/futures/private/position/list";
    } else {
      position_path = "/v2/private/position/list";
    }
  }
  UrlParamsBuilder builder;
  builder.PutGet("symbol", native_symbol);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(position_path, &builder);
  Push(1, ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryAccount());

  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::QueryOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  const auto& native_symbol = params.symbol();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryOrder())) {
    LOG(ERROR) << "Hit rate limit. QueryOrders failed";
    return {};
  }
  std::string all_orders_path = "/private/linear/order/search";
  if (!is_linear_) {
    if (IsDeliveryNativeSymbol(native_symbol)) {
      all_orders_path = "/futures/private/order";
    } else {
      all_orders_path = "/v2/private/order";
    }
  }
  UrlParamsBuilder builder;
  builder.PutGet("symbol", native_symbol);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(all_orders_path, &builder);
  Push(1, ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryOrder());

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::QueryFills(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  const auto& native_symbol = params.symbol();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryTrade())) {
    LOG(ERROR) << "Hit rate limit. QueryFills failed";
    return {};
  }
  std::string query_fill_path = "/private/linear/trade/execution/list";
  if (!is_linear_) {
    if (IsDeliveryNativeSymbol(native_symbol)) {
      query_fill_path = "/futures/private/execution/list";
    } else {
      query_fill_path = "/v2/private/execution/list";
    }
  }
  UrlParamsBuilder builder;
  builder.PutGet("symbol", native_symbol);
  builder.PutGet("page", params.page());
  builder.PutGet("limit", params.limit());
  auto* context = GetAuth()->CreateRequestByGetWithSignature(query_fill_path, &builder);
  Push(1, ts, ActionType::QUERY, native_symbol, GetEndpointGroupQueryTrade());

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::PlaceOrder(
    const RestParams& params,
    const OrderSpec& order_spec) {
  auto ts = GetCurrentTimestamp();
  const auto& native_symbol = params.symbol();
  local_rate_tracker_->SetMakerOrder(order_spec.post_only);
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupTradeOrder())) {
    LOG(ERROR) << "Hit rate limit. PlaceOrder failed";
    return {};
  }
  std::string place_order_path = "/private/linear/order/create";
  if (!is_linear_) {
    if (IsDeliveryNativeSymbol(native_symbol)) {
      place_order_path = "/futures/private/order/create";
    } else {
      place_order_path = "/v2/private/order/create";
    }
  }
  UrlParamsBuilder builder;
  builder.PutPost("symbol", native_symbol);
  builder.PutPost("side", params.side());
  builder.PutPost("price", params.price());
  builder.PutPost("qty", params.qty());
  builder.PutPost("order_link_id", params.order_link_id());
  builder.PutPost("order_type", params.order_type());
  builder.PutPost("time_in_force", params.time_in_force());
  if (is_linear_) {
    builder.PutPost("reduce_only", params.reduce_only());
    builder.PutPost("close_on_trigger", "false");
  }
  auto* context = GetAuth()->CreateRequestByPostWithSignature(place_order_path, &builder);
  Push(1, ts, ActionType::PLACE, native_symbol, GetEndpointGroupTradeOrder());

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::AmendOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  const auto& native_symbol = params.symbol();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupTradeOrder())) {
    LOG(ERROR) << "Hit rate limit. AmendOrder failed";
    return {};
  }
  std::string replace_order_path = "/private/linear/order/replace";
  if (!is_linear_) {
    if (IsDeliveryNativeSymbol(native_symbol)) {
      replace_order_path = "/futures/private/order/replace";
    } else {
      replace_order_path = "/v2/private/order/replace";
    }
  }
  UrlParamsBuilder builder;
  builder.PutPost("order_id", params.order_id());
  builder.PutPost("symbol", native_symbol);
  builder.PutPost("p_r_price", params.p_r_price());
  builder.PutPost("p_r_qty", params.p_r_qty());
  auto* context = GetAuth()->CreateRequestByPutWithSignature(replace_order_path, &builder);
  Push(1, ts, ActionType::AMEND, native_symbol, GetEndpointGroupTradeOrder());

  context->query_type = OrderSubTopicId::AMEND_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  const auto& native_symbol = params.symbol();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY, native_symbol, GetEndpointGroupTradeOrder())) {
    LOG(ERROR) << "Hit rate limit. CancelSingleOrder failed";
    return {};
  }
  std::string cancel_order_path = "/private/linear/order/cancel";
  if (!is_linear_) {
    if (IsDeliveryNativeSymbol(native_symbol)) {
      cancel_order_path = "/futures/private/order/cancel";
    } else {
      cancel_order_path = "/v2/private/order/cancel";
    }
  }
  UrlParamsBuilder builder;
  builder.PutPost("symbol", native_symbol);
  CHECK(params.has_order_id() != params.has_order_link_id())
      << "only one of order_id and order_link_id can be set!" << params.DebugString();

  if (params.has_order_id() > 0) {
    builder.PutPost("order_id", params.order_id());
  } else {
    builder.PutPost("order_link_id", params.order_link_id());
  }
  auto* context = GetAuth()->CreateRequestByPostWithSignature(cancel_order_path, &builder);
  Push(1, ts, ActionType::CANCEL, native_symbol, GetEndpointGroupTradeOrder());

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params, order_spec);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelSingleOrder(params);
}

bool BybitFuturesNativePrivateClient::HasEnoughRateLimit(
    int64_t ts,
    ActionType action_type,
    const std::string& symbol,
    const std::string& endpoint) const {
  if (!local_rate_tracker_) {
    return true;
  }
  auto ret = local_rate_tracker_->HasEnoughRateLimit(ts, action_type, symbol, endpoint);
  if (!ret && order_logger_cb_) {
    rate_tracker_->WriteRateLimitReportMessage(order_logger_cb_);
  }

  return ret;
}

void BybitFuturesNativePrivateClient::Push(
    int32_t num_reqs,
    int64_t ts,
    ActionType action_type,
    const std::string& symbol,
    const std::string& endpoint) {
  if (!local_rate_tracker_) {
    return;
  }
  local_rate_tracker_->Push(num_reqs, ts, action_type, symbol, endpoint);
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params, *order_spec);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelSingleOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> BybitFuturesNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryBalance();
    }
    case RestQueryProto::QUERY_ACCOUNT_POSITION: {
      params.set_symbol("BTCUSD");
      return QueryPosition(params);
    }
    case RestQueryProto::QUERY_RECENT_ORDERS: {
      return QueryOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}
